from collections.abc import Iterable
from typing import Callable, Generic, TypeVar

import dash

AnyValue = TypeVar('AnyValue')


class Loader(dash.html.Template, Generic[AnyValue]):
    def __init__(self,
        id: str,
        func: Callable[[], AnyValue],
        triggers: Iterable[dash.Input],
    ):
        loading_store = dash.dcc.Store(id=f"{id}/loading")
        version_store = dash.dcc.Store(id=f"{id}/version", data=0)
        super().__init__(children=[loading_store, version_store])
        self._loading: bool = True
        self._value: AnyValue | None = None
        self._error: Exception | None = None
        self._loading_store = loading_store
        self._version_store = version_store

        @dash.callback(
            dash.Output(loading_store, 'data'),
            *triggers)
        def trigger_loading(*_):
            self._loading = True
            return True

        @dash.callback(
            dash.Output(loading_store, 'data', allow_duplicate=True),
            dash.Output(version_store, 'data'),
            dash.Input(loading_store, 'data'),
            dash.State(version_store, 'data'),
            prevent_initial_call=True)
        def perform_loading(loading, version):
            if not loading:
                raise dash.exceptions.PreventUpdate
            try:
                self._value = func()
                self._error = None
            except Exception as error:
                self._value = None
                self._error = error
            self._loading = False
            return False, version + 1

    @property
    def loading(self) -> bool:
        return self._loading

    @property
    def value(self) -> AnyValue | None:
        return self._value

    @property
    def error(self) -> Exception | None:
        return self._error

    @property
    def status(self):
        if self.loading:
            return 'loading'
        elif self.error:
            return 'error'
        else:
            return 'ready'

    @property
    def status_trigger(self) -> dash.Input:
        return dash.Input(self._loading_store, 'data')

    @property
    def value_trigger(self) -> dash.Input:
        return dash.Input(self._version_store, 'data')
